/*
 * Copyright (C) 2012-2013  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.remote.event;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import nemos.remote.RemoteConstants;
import nemos.remote.util.RemoteUtil;
import nemos.util.PropertyHelper;
import nemos.util.osgi.FilterBuilder;
import nemos.util.osgi.IServiceMap;
import nemos.util.osgi.ServiceHashMap;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;

/**
 * RemoteEventHandler : The remote event handler.
 * 
 * <p>
 * This class should be imported into all connected frameworks in order to
 * listen to all events. Exportable ones are dispatched in all connected
 * frameworks.
 * </p>
 * 
 * <p>
 * An exportable event is an event which has not been already exported and which
 * matches a additonal filter specified at registration.
 * </p>
 * 
 * <p>
 * It is recommended to activate only one {@link RemoteEventHandler} in all
 * connected frameworks and to active it on the most 'central' one (the server
 * if applicable).
 * </p>
 * 
 * @author Romain DUBOIS
 */
@Properties({
        @Property(name = Constants.SERVICE_EXPORTED_INTERFACES, value = "*"),
        @Property(name = Constants.SERVICE_EXPORTED_INTENTS, value = RemoteConstants.INTERCEPTION_INTENT) })
@Component(name = RemoteEventHandler.SERVICE_PID, policy = ConfigurationPolicy.REQUIRE)
public class RemoteEventHandler implements EventHandler {

    /** This service's PID */
    static final String SERVICE_PID = "nemos.remote.event.handler"; //$NON-NLS-1$
    /** The required event filter */
    private static final String FILTER = FilterBuilder.getStringFilter(
            RemoteConstants.SOURCE_FRAMEWORK, "*", true); //$NON-NLS-1$

    /**
     * Create a event copying the provided one and with the source framework
     * property filled.
     * 
     * @param pEvent
     *            The event to copy
     * @param pFrameworkUuid
     *            The source framework
     * @return The event copy
     */
    public static Event addFramework(final Event pEvent, final String pFrameworkUuid) {

        // Copy properties
        final Map<String, Object> lProps = new HashMap<String, Object>();
        for (final String lPropName : pEvent.getPropertyNames()) {
            lProps.put(lPropName, pEvent.getProperty(lPropName));
        }

        // Add source framework
        lProps.put(RemoteConstants.SOURCE_FRAMEWORK, pFrameworkUuid);

        // Create event copy
        return new Event(pEvent.getTopic(), lProps);
    }

    /** The broadcasters */
    @Reference(name = "broadcaster", cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC, referenceInterface = IEventBroadcaster.class)
    private IServiceMap<IEventBroadcaster> mBroadcasters;
    /** The executor */
    private ExecutorService mExecutor;
    /** The component registration */
    private ServiceRegistration<EventHandler> mRegistration;

    /**
     * Constructor
     */
    public RemoteEventHandler() {
        super();
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event)
     */
    @Override
    public void handleEvent(final Event pEvent) {
        Object lSourceFramework = pEvent.getProperty(RemoteConstants.SOURCE_FRAMEWORK);

        // Local event are never intercepted
        final Event lEvent;
        if (lSourceFramework == null) {
            lSourceFramework = RemoteUtil.getFrameworkUuid();
            lEvent = addFramework(pEvent, (String) lSourceFramework);
        } else {
            lEvent = pEvent;
        }

        // Collect copy for non-blocking read
        Collection<String> lFrameworks;
        synchronized (mBroadcasters) {
            lFrameworks = new HashSet<String>(mBroadcasters.keySet());
        }

        // Call each other framework
        lFrameworks.remove(lSourceFramework);
        for (final String lFramework : lFrameworks) {
            mExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    broadcast(lFramework, lEvent);
                }
            });
        }
    }

    /**
     * Broadcast an event to a specific framework.
     * 
     * @param pFramework
     *            The target framework UUID
     * @param pEvent
     *            The event to broadcast
     */
    void broadcast(final String pFramework, final Event pEvent) {
        final IEventBroadcaster lBroadcaster = mBroadcasters.getService(pFramework);

        // Broadcaster may have been unregistered
        if (lBroadcaster != null) {
            lBroadcaster.broadcastEvent(pEvent);
        }
    }

    /**
     * Start the component.
     * 
     * @param pContext
     *            The component context
     */
    @Activate
    public void start(final ComponentContext pContext) {
        mExecutor = Executors.newCachedThreadPool();
        mBroadcasters = new ServiceHashMap<IEventBroadcaster>(
                org.osgi.service.remoteserviceadmin.RemoteConstants.ENDPOINT_FRAMEWORK_UUID,
                pContext.getBundleContext());

        @SuppressWarnings("unchecked")
        final Hashtable<String, Object> lProps = PropertyHelper.copy(pContext.getProperties());

        // Add required filter (in order to avoid endless loop)
        String lFilter;
        if (lProps.get(EventConstants.EVENT_FILTER) == null) {
            lFilter = FILTER;
        } else {
            final FilterBuilder lBuilder = new FilterBuilder(FILTER);
            lBuilder.and(new FilterBuilder((String) lProps.get(EventConstants.EVENT_FILTER)));
            lFilter = lBuilder.toString();
        }
        lProps.put(EventConstants.EVENT_FILTER, lFilter);
        lProps.put(Constants.SERVICE_PID, SERVICE_PID);

        // Register service
        mRegistration = pContext.getBundleContext().registerService(EventHandler.class, this,
                lProps);
    }

    /**
     * Stop the component.
     */
    public void stop() {
        mRegistration.unregister();
        mExecutor.shutdown();
    }

    /**
     * Bind a broadcaster.
     * 
     * @param pReference
     *            The service reference
     */
    public void bindBroadcaster(final ServiceReference<IEventBroadcaster> pReference) {
        synchronized (mBroadcasters) {
            mBroadcasters.add(pReference);
        }
    }

    /**
     * Unbind a broadcaster.
     * 
     * @param pReference
     *            The service reference
     */
    public void unbindBroadcaster(final ServiceReference<IEventBroadcaster> pReference) {
        synchronized (mBroadcasters) {
            mBroadcasters.remove(pReference);
        }
    }
}
